package io.demo.message.domain.proto;

import com.google.common.collect.ImmutableMap;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

public class MessageConverters {

  private static final MessageConverterRegistry REGISTRY;

  private static final Map<Class<?>, Class<? extends Message>> DOMAIN_MESSAGE_MAPPING;

  private static final ConcurrentHashMap<Class<?>, Method> METHOD_CACHE = new ConcurrentHashMap<>();

  private static final ExtensionRegistry EXTENSION_REGISTRY = ExtensionRegistry.newInstance();


  private MessageConverters() {
  }

  static <B, M extends Message> MessageConverter<B, M> get(Class<B> bClass) {
    MessageConverter<B, M> cc = REGISTRY.get(bClass);
    if (Objects.isNull(cc)) {
      String message = MessageFormat.format("Unknown registry for {0}", bClass);
      throw new IllegalStateException(message);
    }
    return cc;
  }

  @SuppressWarnings("unchecked")
  public static <B, M extends Message> M toMessage(B b, Class<B> bClass) {
    return (M) get(bClass).toMessage(b);
  }

  public static <B, M extends Message> B fromMessage(M m, Class<B> bClass) {
    return get(bClass).fromMessage(m);
  }

  public static <T, R extends Message> R parseFrom(byte[] byteArray, Class<T> clazz) {
    @SuppressWarnings("unchecked")
    Class<R> rClass = (Class<R>) DOMAIN_MESSAGE_MAPPING.get(clazz);
    try {
      Message.Builder builder = getMessageBuilder(rClass);
      @SuppressWarnings("unchecked")
      R rr = (R) builder.mergeFrom(byteArray, EXTENSION_REGISTRY).build();
      return rr;
    } catch (IllegalAccessException | InvocationTargetException | InvalidProtocolBufferException e) {
      throw new MessageParseException(e);
    }
  }

  private static Message.Builder getMessageBuilder(Class<? extends Message> clazz)
      throws InvocationTargetException, IllegalAccessException {
    Method method = METHOD_CACHE.computeIfAbsent(clazz, key -> {
      try {
        return key.getMethod("newBuilder");
      } catch (NoSuchMethodException e) {
        throw new MessageParseException(e);
      }
    });
    return (Message.Builder) method.invoke(clazz);
  }

  static {
    REGISTRY = MessageConverterRegistry.empty()
        .register(io.demo.message.domain.Area.class, new AreaMessageConverter())
        .register(io.demo.message.domain.City.class, new CityMessageConverter())
        .register(io.demo.message.domain.Ethnic.class, new EthnicMessageConverter())
        .register(io.demo.message.domain.Country.class, new CountryMessageConverter())
        .register(io.demo.message.domain.Address.class, new AddressMessageConverter())
        .register(io.demo.message.domain.Customer.class, new CustomerMessageConverter());

    DOMAIN_MESSAGE_MAPPING = new ImmutableMap.Builder<Class<?>, Class<? extends Message>>()
        .put(io.demo.message.domain.Area.class, io.demo.message.domain.proto.Area.class)
        .put(io.demo.message.domain.City.class, io.demo.message.domain.proto.City.class)
        .put(io.demo.message.domain.Ethnic.class, io.demo.message.domain.proto.Ethnic.class)
        .put(io.demo.message.domain.Country.class, io.demo.message.domain.proto.Country.class)
        .put(io.demo.message.domain.Address.class, io.demo.message.domain.proto.Address.class)
        .put(io.demo.message.domain.Customer.class, io.demo.message.domain.proto.Customer.class)
        .build();
  }
}

class MessageParseException extends RuntimeException {

  private static final long serialVersionUID = 3096794755769844926L;

  MessageParseException(Throwable cause) {
    super(cause);
  }
}